﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

namespace Tests
{
    [TestFixture]
    public class SortedSets // http://redis.io/commands#sorted_set
    {
        [Test]
        public void Range() // http://code.google.com/p/booksleeve/issues/detail?id=12
        {
            using(var conn = Config.GetUnsecuredConnection())
            {
                const double value = 634614442154715;
                conn.Keys.Remove(3, "zset");
                conn.SortedSets.Add(3, "zset", "abc", value);
                var range = conn.SortedSets.Range(3, "zset", 0, -1);

                Assert.AreEqual(value, conn.Wait(range).Single().Value);
            }
        }
        [Test]
        public void RangeWithInfiniteMax() 
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                const double value = 634614442154715;
                conn.Keys.Remove(3, "zset");
                conn.SortedSets.Add(3, "zset", "abc", value);
                var range = conn.SortedSets.Range(3, "zset", 1, double.PositiveInfinity);

                Assert.AreEqual(value, conn.Wait(range).Single().Value);
            }
        }
        [Test]
        public void RangeWithInfiniteMaxShouldExcludeValues()
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                const double value = 634614442154715;
                conn.Keys.Remove(3, "zset");
                conn.SortedSets.Add(3, "zset", "abc", value);
                var range = conn.SortedSets.Range(3, "zset", value+1, double.PositiveInfinity);

                Assert.IsFalse(conn.Wait(range).Any());
            }
        }
        [Test]
        public void RangeWithInfiniteMin()
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                const double value = 634614442154715;
                conn.Keys.Remove(3, "zset");
                conn.SortedSets.Add(3, "zset", "abc", value);
                var range = conn.SortedSets.Range(3, "zset", double.NegativeInfinity, value+1);

                Assert.AreEqual(value, conn.Wait(range).Single().Value);
            }
        }
        [Test]
        public void RangeWithInfiniteMinShouldExcludeValues()
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                const double value = 634614442154715;
                conn.Keys.Remove(3, "zset");
                conn.SortedSets.Add(3, "zset", "abc", value);
                var range = conn.SortedSets.Range(3, "zset", double.NegativeInfinity, value - 1);

                Assert.IsFalse(conn.Wait(range).Any());
            }
        }

        [Test]
        public void Score() // http://code.google.com/p/booksleeve/issues/detail?id=23
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                const double value = 634614442154715;
                conn.Keys.Remove(3, "zset");
                conn.SortedSets.Add(3, "zset", "abc", value);
                var score = conn.SortedSets.Score(3, "zset","abc");
                Assert.AreEqual(value, conn.Wait(score));
            }
        }
        [Test]
        public void ScoreOnNonexistentValue_MustReturnNull() // http://code.google.com/p/booksleeve/issues/detail?id=23
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                const double value = 634614442154715;
                conn.Keys.Remove(3, "zset");
                conn.SortedSets.Add(3, "zset", "abc", value);
                var score = conn.SortedSets.Score(3, "zset", "xyz");
                Assert.AreEqual(null, conn.Wait(score));
            }
        }


        [Test]
        public void ZInterStore()
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                conn.Keys.Remove(3, new[] { "A", "B", "C" });

                conn.SortedSets.Add(3, "A", "OBJ1", 1);
                conn.SortedSets.Add(3, "B", "OBJ1", 1);

                var intersectAndStore = conn.SortedSets.ZIntersectAndStore(3, "C", new[] { "A", "B" });
                Assert.AreEqual(1, conn.Wait(intersectAndStore));

                var range = conn.SortedSets.Range(3, "C", 0, 2);
                var pair = conn.Wait(range).Single();
                Assert.AreEqual("OBJ1", Encoding.UTF8.GetString(pair.Key));
                Assert.AreEqual(2.0, pair.Value); // SUM

                intersectAndStore = conn.SortedSets.ZIntersectAndStore(3, "C", new[] { "A", "B" });
                Assert.AreEqual(1, conn.Wait(intersectAndStore));
                range = conn.SortedSets.Range(3, "C", 0, 6);
                pair = conn.Wait(range).Single();
                Assert.AreEqual("OBJ1", Encoding.UTF8.GetString(pair.Key));
                Assert.AreEqual(2.0, pair.Value); // SUM
            }
        }

        [Test]
        public void ZUnionStore()
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                conn.Keys.Remove(3, new[] { "A", "B", "C" });

                conn.SortedSets.Add(3, "A", "OBJ1", 1);
                conn.SortedSets.Add(3, "B", "OBJ1", 2);
                conn.SortedSets.Add(3, "B", "OBJ2", 1);

                var unionAndStore = conn.SortedSets.ZUnionAndStore(3, "C", new[] { "A", "B" });
                Assert.AreEqual(2, conn.Wait(unionAndStore));

                var range = conn.SortedSets.Range(3, "C", 0, 2);
                var pairs = conn.Wait(range);
                Assert.AreEqual(2, pairs.Length);

                var dict = pairs.ToDictionary(x => Encoding.UTF8.GetString(x.Key), x => x.Value);
                Assert.IsTrue(dict.ContainsKey("OBJ1"));
                Assert.IsTrue(dict.ContainsKey("OBJ2"));
                Assert.AreEqual(3.0, dict["OBJ1"]);
                Assert.AreEqual(1.0, dict["OBJ2"]);
            }
        }
        [Test]
        public void ZUnionStoreWithWeights()
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                conn.Keys.Remove(3, new[] { "A", "B", "C" });

                conn.SortedSets.Add(3, "A", "OBJ1", 1);
                conn.SortedSets.Add(3, "B", "OBJ1", 2);
                conn.SortedSets.Add(3, "B", "OBJ2", 1);

                var unionAndStore = conn.SortedSets.ZUnionAndStore(3, "C", new[] { "A", "B" },new double[]{1,2});
                Assert.AreEqual(2, conn.Wait(unionAndStore));

                var range = conn.SortedSets.Range(3, "C", 0, 2);
                var pairs = conn.Wait(range);
                Assert.AreEqual(2, pairs.Length);

                var dict = pairs.ToDictionary(x => Encoding.UTF8.GetString(x.Key), x => x.Value);
                Assert.IsTrue(dict.ContainsKey("OBJ1"));
                Assert.IsTrue(dict.ContainsKey("OBJ2"));
                Assert.AreEqual(5.0, dict["OBJ1"]);
                Assert.AreEqual(2.0, dict["OBJ2"]);
            }
        }

        [Test]
        public void ZIntersectDivideAndStore()
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                conn.Keys.Remove(3, new[] { "A", "B", "C" });

                conn.SortedSets.Add(3, "A", "OBJ1", 1);
                conn.SortedSets.Add(3, "A", "OBJ2", 2);
                conn.SortedSets.Add(3, "A", "OBJ3", 3);

                conn.SortedSets.Add(3, "B", "OBJ1", 4);
                conn.SortedSets.Add(3, "B", "OBJ2", 5);
                conn.SortedSets.Add(3, "B", "OBJ4", 6);

                var task = conn.SortedSets.ZIntersectDivideAndStore(3, "C", new [] { "A", "B" });
                var affected = conn.Wait(task);

                Assert.AreEqual(2, affected);

                var taskAll = conn.SortedSets.Range(3, "C", 0, -1);
                var all = conn.Wait(taskAll);

                Assert.AreEqual(2, all.Length);

                var pair = all[0];
                Assert.AreEqual("OBJ1", Decode(pair.Key));
                Assert.AreEqual(0.25, pair.Value);

                pair = all[1];
                Assert.AreEqual("OBJ2", Decode(pair.Key));
                Assert.AreEqual(0.4, pair.Value);
            }
        }

        [Test]
        public void ZUnionDivideAndStore()
        {
            using (var conn = Config.GetUnsecuredConnection())
            {
                conn.Keys.Remove(3, new[] { "A", "B", "C" });

                conn.SortedSets.Add(3, "A", "OBJ1", 1);
                conn.SortedSets.Add(3, "A", "OBJ2", 2);
                conn.SortedSets.Add(3, "A", "OBJ3", 3);

                conn.SortedSets.Add(3, "B", "OBJ1", 4);
                conn.SortedSets.Add(3, "B", "OBJ2", 5);
                conn.SortedSets.Add(3, "B", "OBJ4", 6);

                var task = conn.SortedSets.ZUnionDivideAndStore(3, "C", new [] { "A", "B" });
                var affected = conn.Wait(task);

                Assert.AreEqual(4, affected);

                var taskAll = conn.SortedSets.Range(3, "C", 0, -1);
                var all = conn.Wait(taskAll);

                Assert.AreEqual(4, all.Length);

                var pair = all[0];
                Assert.AreEqual("OBJ3", Decode(pair.Key));
                Assert.AreEqual(0, pair.Value);
                
                pair = all[1];
                Assert.AreEqual("OBJ4", Decode(pair.Key));
                Assert.AreEqual(0, pair.Value);

                pair = all[2];
                Assert.AreEqual("OBJ1", Decode(pair.Key));
                Assert.AreEqual(0.25, pair.Value);

                pair = all[3];
                Assert.AreEqual("OBJ2", Decode(pair.Key));
                Assert.AreEqual(0.4, pair.Value);
            }
        }

        private int[] GetPerfTestSetSizes()
        {
            return new[] {
                1, 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 11000, 12000, 
                13000, 14000, 15000, 16000, 17000, 18000, 19000, 20000, 25000, 30000, 35000, 40000, 50000,
                60000, 75000, 100000
            };
        }

        [Test]
        [Ignore]
        public void Stage_Performance_Test_Data()
        {
            var sizes = GetPerfTestSetSizes();

            using (var conn = Config.GetUnsecuredConnection())
            {
                foreach (var size in sizes)
                {
                    Debug.WriteLine("Staging data for size: " + size + "...");

                    var rnd = new Random();
                    var taskList = new List<Task>();

                    for (var i = 0; i < size; i++)
                    {
                        taskList.Add(conn.SortedSets.Add(5, "A" + size, "V" + i, rnd.NextDouble()*1000));
                        taskList.Add(conn.SortedSets.Add(5, "B" + size, "V" + i, rnd.NextDouble()*1000));
                    }

                    conn.WaitAll(taskList.ToArray());

                    Debug.WriteLine("Done. Pausing...");
                    Thread.Sleep(5000);
                }
            }
        }

        [Test]
        public void ZUnionDivideAndStore_Performance()
        {
            var sizes = GetPerfTestSetSizes();

            var sb = new StringBuilder();
            sb.AppendLine("Size,Milliseconds");

            using (var conn = Config.GetUnsecuredConnection())
            {
                foreach (var size in sizes)
                {
                    Debug.WriteLine("Beginning run for size: " + size + "...");
                    Debug.WriteLine("Clearing results...");
                    conn.Keys.Remove(5, new[] {"C"});
                    Debug.WriteLine("Done. Pausing...");
                    Thread.Sleep(5000);

                    Debug.WriteLine("Dividing sets...");
                    var task = conn.SortedSets.ZUnionDivideAndStore(5, "C", new[] {"A" + size, "B" + size});
                    //var task = conn.SortedSets.ZUnionDivideAndStore(5, "C", new[] {"A100000", "B1000"});
                    //var task = conn.SortedSets.ZUnionAndStore(5, "C", new[] {"A" + size, "B" + size}, new [] { 1.0, 1.0 }, "div");

                    var stopWatch = new Stopwatch();
                    stopWatch.Start();
                    var affected = conn.Wait(task);
                    stopWatch.Stop();
                    Debug.WriteLine("Done. Affected: " + affected);
                    Thread.Sleep(5000);
                    Debug.WriteLine("Done.");

                    sb.AppendLine(size + "," + stopWatch.ElapsedMilliseconds);
                }
            }

            var csv = sb.ToString();
            Assert.IsTrue(true);
        }

        [Test]
        public void RangeByScoreWithoutScoresAndStore_Ascending() {
            using (var conn = Config.GetUnsecuredConnection()) {
                conn.Keys.Remove(3, new[] { "A", "DEST" });

                conn.SortedSets.Add(3, "A", "OBJ1", 1);
                conn.SortedSets.Add(3, "A", "OBJ2", 2);
                conn.SortedSets.Add(3, "A", "OBJ3", 3);
                conn.SortedSets.Add(3, "A", "OBJ4", 4);
                conn.SortedSets.Add(3, "A", "OBJ5", 5);
                conn.SortedSets.Add(3, "A", "OBJ6", 6);

                var task = conn.SortedSets.RangeByScoreWithoutScoresAndStore(3, "DEST", "A", 2, 5);
                var affected = conn.Wait(task);

                Assert.AreEqual(4, affected);
                
                var taskAll = conn.Sets.GetAll(3, "DEST");
                var all = conn.Wait(taskAll);

                Assert.AreEqual(4, all.Length);

                // Order of these could change depending on Redis's implementation
                Assert.AreEqual("OBJ2", Decode(all[0]));
                Assert.AreEqual("OBJ3", Decode(all[1]));
                Assert.AreEqual("OBJ4", Decode(all[2]));
                Assert.AreEqual("OBJ5", Decode(all[3]));
            }
        }

        static string Decode(byte[] value) { return Encoding.UTF8.GetString(value); }
    }
}
